﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using Ping.Core;

namespace Ping.App
{
	public enum ConnectionState
	{
		/// <summary>
		/// The connection is idle and doesn't do anything at the moment.
		/// </summary>
		Idle,

		/// <summary>
		/// The connection is currently trying to establish a connection with the server.
		/// </summary>
		Connecting,

		/// <summary>
		/// The connection with the server has been established.
		/// </summary>
		Connected,
	}

	public delegate void RequestHandler(Result result, Exception e);

	/// <summary>
	/// Manages a connection between the client & server on the client-side.
	/// The connection can be dropped at any time and is established whenever necessary.
	/// </summary>
	/// <remarks>
	/// The inner workings of this class are overly complex because of the crippled socket api
	/// under windows phone (every communication is async. although the underlying protocol is
	/// based on a simple request => response scheme which doesn't allow multiple requests at a time).
	/// </remarks>
	public sealed class Connection
	{
		private readonly Channel _channel;
		private readonly Dictionary<Request, Operation> _operations;

		private string _host;
		private int _port;

		public int Count
		{
			get { return _operations.Count; }
		}

		/// <summary>
		/// Creates a new (idle) connection.
		/// </summary>
		/// <remarks>
		/// This method is thread-safe.
		/// </remarks>
		public Connection()
		{
			_channel = new Channel();
			_operations = new Dictionary<Request, Operation>();

			Connect();
		}

		public ConnectionState State
		{
			get { return _channel.IsConnected ? ConnectionState.Connected : ConnectionState.Idle; }
		}

		/// <summary>
		/// This event is invoked whenever the status has changed.
		/// </summary>
		public event Action StateChanged;

		/// <summary>
		/// Sends a request to the server, awaiting it's result.
		/// </summary>
		/// <param name="request"></param>
		/// <param name="handler"></param>
		/// <returns></returns>
		public void Request(Request request, RequestHandler handler)
		{
			lock (this)
			{
				if (request == null || handler == null)
					throw new ArgumentNullException();
				if (_operations.ContainsKey(request))
					throw new ArgumentException();

				_operations.Add(request, new Operation(request, handler));
				if (!_channel.IsConnected)
					Connect();
				else
					Begin(request);
			}
		}

		private void Begin(Request request)
		{
			Operation op;
			if (!_operations.TryGetValue(request, out op))
				return;

			if (op.Begun)
				return;

			_channel.BeginSend(request, request, result => Handler(result, EndSend));
		}

		/// <summary>
		/// Tries to establish a connection with the application server.
		/// Uses the current settings of the SettingsService.
		/// </summary>
		public void Connect()
		{
			lock (this)
			{
				Settings settings = SettingsService.Settings;
				_host = settings.AppServer;
				_port = settings.AppPort;

				var ep = new DnsEndPoint(_host, _port);
				_channel.BeginConnect(ep, null, result => Handler(result, EndConnect));
			}
		}

		private void EndConnect(IAsyncResult result)
		{
			try
			{
				_channel.EndConnect(result);

				// Begin all pending requests...
				foreach (var request in _operations.Keys)
				{
					Begin(request);
				}
			}
			catch (Exception e)
			{
				foreach (var op in _operations.Values)
				{
					if (!op.Begun)
						op.Handler(null, e);
				}

				_operations.Clear();
			}
		}

		/// <summary>
		/// Closes the connection.
		/// </summary>
		public void Disconnect()
		{
			lock (this)
			{
				_channel.Disconnect();
			}
		}

		private void EndSend(IAsyncResult result)
		{
			var request = (Request)result.AsyncState;
			Operation op;
			if (!_operations.TryGetValue(request, out op))
				return;

			_channel.EndSend(result);
			_channel.BeginReceive(request, nextResult => Handler(nextResult, EndReceive));
		}

		private void EndReceive(IAsyncResult result)
		{
			var request = (Request)result.AsyncState;
			Operation op;
			if (!_operations.TryGetValue(request, out op))
				return;

			Data data = _channel.EndReceive(result);
			op.Handler((Result)data, null);

			_operations.Remove(request);
		}

		private void Handler(IAsyncResult result, AsyncCallback callback)
		{
			lock (this)
			{
				try
				{
					callback(result);
				}
				catch (Exception e)
				{
					_channel.Disconnect();

					// Simply close all operations...

					foreach (var op in _operations.Values)
					{
						op.Handler(null, e);
					}

					_operations.Clear();
				}
			}
		}
	}

	internal class Operation
	{
		public readonly RequestHandler Handler;
		public readonly Request Request;
		public Result Result;
		public bool Begun;

		public Operation(Request r, RequestHandler h)
		{
			Request = r;
			Handler = h;
		}
	}
}